package com.delta.rafg.core.database;

import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.servlet.ServletContext;

import com.delta.rafg.core.config.ConfigSettings;
import com.delta.rafg.core.database.IDataHandler.POLL_STYLE;
import com.delta.rafg.core.database.impl.util.DatabaseUtils;
import com.delta.rafg.core.exception.RAFGException;
import com.delta.rafg.core.log.RAFGLogger;

public class CacheController {

    /**
     * Flag to turn on local debugging.
     */
//    private static boolean LOCAL_DBG = true;

    private SynchroHandler synchroHandler = null;
    private ConfigSettings configSettings = null;

    private Thread databaseMaintainerThread = null;
    private boolean stopDatabaseMaintainerThread = false;

    private static CacheController cacheControllerSingleton = null;

    private ServletContext context;
    
    private static int sleepAfterErrorMins = 5;

    private Object objLock = new Object();
    
    /**
     * Sync interval in minutes.
     */
//    private int syncInterval = 30;

    private Queue<String> tableSyncReqQueue = new ConcurrentLinkedQueue<String>();  
    private static final String ALL_TABLES = "<<<ALL>>>";
    
    public CacheController() throws RAFGException {
    	this(ConfigSettings.getSingleton());
/*
        try {
			startMaintainer();
		} catch (RAFGException e) {
			RAFGLogger.fatal("Could not properly instantiate CacheController()", e, null);
		}
*/
    }

    public CacheController(ConfigSettings configSettings) throws RAFGException {
        this.configSettings = configSettings;
/*
        try {
			startMaintainer();
		} catch (RAFGException e) {
			RAFGLogger.fatal("Could not properly instantiate CacheController()", e, null);
		}
*/
    }

    public void stopMaintainer() {
        stopDatabaseMaintainerThread = true;
        if (databaseMaintainerThread != null) {
        	databaseMaintainerThread.interrupt();
        	databaseMaintainerThread = null;
        }
    }

    public void startMaintainer() throws RAFGException {
    	synchronized (objLock) {
	        if (databaseMaintainerThread == null) {
	            if (synchroHandler == null) {
	                createSynchroHandler();
	            }
	            databaseMaintainerThread = createMaintainerThread();
	            databaseMaintainerThread.setDaemon(true);

	            //set to false so the thread will run once and exit.  this needs to be fixed to use websphere to manage this thread.
	            stopDatabaseMaintainerThread = false;

	            databaseMaintainerThread.start();
	        }
    	}
    }
    
    public void setServletContext(ServletContext servletContext) {
    	context = servletContext;
    }

    private Thread createMaintainerThread() {
        return new Thread(new MaintainerThread(), "RafgCacheController");
    }

    public static synchronized void addGenericDataSource(String datasourceClassStr) {
    	try {
			Class<?> c = Class.forName(datasourceClassStr);
			if (c.isAssignableFrom(c)) {
				addGenericDataSource((IGenericDataSource) c.newInstance());
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InstantiationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
    	
    }
    
    public static synchronized void addGenericDataSource(IGenericDataSource datasource) {
    	try {
			CacheController singletonController = asSingleton();
            if (singletonController.synchroHandler == null) {
            	singletonController.createSynchroHandler();
            }
            singletonController.synchroHandler.addGenericDataSource(datasource);
		} catch (RAFGException e) {
			e.printStackTrace();
		}
    }
    
    public static synchronized CacheController asSingleton() throws RAFGException {
    	if (cacheControllerSingleton == null) {
    		cacheControllerSingleton = new CacheController();
    	}
    	return cacheControllerSingleton; 
    }

    public static void syncronizeNow() {
    	try {
			asSingleton().tableSyncReqQueue.add(ALL_TABLES);
		} catch (RAFGException e) {
			RAFGLogger.error("Could not synchronize cache tables.", e, null);
		}
    }
    
    public static void synchronizeTable(String table) {
    	//remove _CACHE from schema name if it was passed.
    	String schemaName = DatabaseUtils.getSchemaName(table);
    	String tableName = DatabaseUtils.getTableName(table);
    	
    	if (schemaName.toUpperCase().endsWith("_CACHE")) {
    		schemaName = schemaName.substring(0, schemaName.length()-6);
    	}
    	
    	try {
			asSingleton().tableSyncReqQueue.add(schemaName + "." + tableName);
		} catch (RAFGException e) {
			RAFGLogger.error("Could not synchronize cache table " + table, e, null);
		}
    }
    
    public void destroy() {
    	RAFGLogger.info("CacheController.destroy() stopping RafgCacheMaintainer thread.");
    	stopMaintainer();
    	synchroHandler.destroy();
    	synchroHandler = null;
    	cacheControllerSingleton = null;
    }

    IDataHandler getCacheDataHandler() {
    	return cacheControllerSingleton.synchroHandler.getCacheDataHandler();
    }

    IDataHandler getReferenceDataHandler() {
    	return getReferenceDataHandler(null);
    }
    
    IDataHandler getReferenceDataHandler(String identifier) {
    	List<IDataHandler> masterDataHandlers = cacheControllerSingleton.synchroHandler.getMasterDataHandlers();
    	IDataHandler retHandler = null;
    	if (masterDataHandlers != null && masterDataHandlers.size() >= 1) {
    		if (identifier != null) { 
    			retHandler = masterDataHandlers.get(0);
    		}
    		else {
    			for (IDataHandler d : masterDataHandlers) {
    				if (d.getDataHandlerOptions().getDataHandlerPropertyIdentifier().equalsIgnoreCase(identifier)) {
    					retHandler = d;
    					break;
    				}
    			}
    		}
    	}
    	return retHandler;
    }

    private synchronized void createSynchroHandler() throws RAFGException {
        if (synchroHandler == null) {
            synchroHandler = new SynchroHandler();
            synchroHandler.setServletContext(context);
            synchroHandler.initialize(configSettings);
        }
    }

    public class MaintainerThread implements Runnable {

        private Map<IDataHandler, Long> lastSyncCheckMap = new HashMap<IDataHandler, Long>();

//        private long lastNightlyFullSyncRun = 0;
//        private long lastSyncCheck = 0;

        @Override
        public void run() {

            do {
                try {
                	Map<IDataHandler, String[]> changedMasterTables = synchroNeeded();
                	if (changedMasterTables != null && !changedMasterTables.isEmpty()) {
                		StringBuilder sb = new StringBuilder();
                		for (String ss[] : changedMasterTables.values()) {
                			for (String s : ss) {
                				sb.append(s + "; ");
                			}
                		}
//                		RAFGLogger.info("Synchronizing the following tables: " + sb.toString());
                        syncDatabases(changedMasterTables, lastSyncCheckMap);
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                } catch (RAFGException e) {
//                    e.printStackTrace();
                    try {
						Thread.sleep(sleepAfterErrorMins * 60000);
					} catch (InterruptedException e1) {
					}
                } catch (Exception e) {
                    //we want to catch all exceptions here, do not want thread to end prematurely.
                    //e.printStackTrace();
					RAFGLogger.error(e, (Object[]) null);
                    try {
						Thread.sleep(sleepAfterErrorMins * 60000);
					} catch (InterruptedException e1) {
					}
                }
            } while (!stopDatabaseMaintainerThread);
            RAFGLogger.info("CacheController MaintainerThread closing, goodbye.");   
        }

        private Map<IDataHandler, String[]> synchroNeeded() throws RAFGException, SQLException, ClassNotFoundException {
            Map<IDataHandler, String[]> changedMasterTables = null;

            //if sync is disabled, always return nothing to be synced
    		if (!Boolean.parseBoolean(configSettings.getProperty(SynchroHandler.CONFIG_SETTINGS_PREFIX + ".enable", "true"))) {
    			return null;
    		}

            if (!tableSyncReqQueue.isEmpty()) {
            	List<String> tables = new ArrayList<String>();
				while(!tableSyncReqQueue.isEmpty()) {
					String table = tableSyncReqQueue.peek();
	    			if (table.equals(ALL_TABLES)) {
	    				tables.clear();
	    				break;
	    			}
	    			tableSyncReqQueue.poll();
					tables.add(table);
				}
				if (tables.size() == 0) {
					String table = tableSyncReqQueue.poll();
					if (table.equals(ALL_TABLES)) {
						synchroHandler.syncAllTables();
					}
				} else {
       				changedMasterTables = findMasterTableSources(tables);
            	}
            	
            } else {
            	
            	//see if any of the master datahandlers are scheduled to be synchronized.
            	List<IDataHandler> masterDataHandlers = synchroHandler.getMasterDataHandlers();
            	for (IDataHandler mdh : masterDataHandlers) {
                	Long lastSyncCheck = lastSyncCheckMap.get(mdh);
                	if (lastSyncCheck == null) {
                		lastSyncCheck = 0L;
                	}

    	            if (mdh.getPollStyle() == POLL_STYLE.INTERVAL) {
        	            Calendar checkCal = Calendar.getInstance();
        	            
    	            	int interval = Integer.parseInt(mdh.getPollFrequency());
    	            	if (interval > 0) {
    	            		checkCal.add(Calendar.MINUTE, 0 - interval);
    	    	            long checkTime = checkCal.getTimeInMillis();
/*   	    	            
    	    	            DateFormat debugDf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    	    	            RAFGLogger.info("(" + mdh.getDataHandlerOptions().getDataHandlerPropertyIdentifier() + ") CheckTime: " + debugDf.format(checkCal.getTime()));
    	    	            java.util.Date debugDt = new java.util.Date(lastSyncCheck);
    	    	            RAFGLogger.info("(" + mdh.getDataHandlerOptions().getDataHandlerPropertyIdentifier() + ") lastSyncCheck: " + debugDf.format(debugDt));
*/
    	    	            if (checkTime > lastSyncCheck) {
    	    	            	changedMasterTables = synchroHandler.getChangedMasterTables(mdh);
    	    	            	//if nothing has changed since the last time this was checked, just update  here so it will wait until the next cycle
    	    	            	if (changedMasterTables == null || changedMasterTables.isEmpty()) {
    	    	            		lastSyncCheckMap.put(mdh, Calendar.getInstance().getTimeInMillis());
    	    	            	} else {
    	    	            		break;
    	    	            	}
    	    	            }

    	            	}
    	            } else if (mdh.getPollStyle() == POLL_STYLE.TIMESTAMP) {
    	            	Calendar currTime = Calendar.getInstance();
    	            	DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
    	            	String schdTimeTodayStr = df.format(currTime.getTime()) + " " + mdh.getPollFrequency();
    	            	df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    	            	Calendar schdTimeToday = Calendar.getInstance(); 
    	            	try {
    	            		schdTimeToday.setTime(df.parse(schdTimeTodayStr));
						} catch (ParseException e) {
							throw new RAFGException(e);
						}
						if (schdTimeToday.before(currTime) && lastSyncCheck < schdTimeToday.getTimeInMillis()) {
							changedMasterTables = synchroHandler.getChangedMasterTables(mdh);
	    	            	//if nothing has changed since the last time this was checked, just update  here so it will wait until the next cycle
	    	            	if (changedMasterTables == null || changedMasterTables.isEmpty()) {
	    	            		lastSyncCheckMap.put(mdh, Calendar.getInstance().getTimeInMillis());
	    	            	} else {
	    	            		break;
	    	            	}
						}
    	            }
            	}
            }
            return changedMasterTables;
        }

        private Map<IDataHandler, String[]> findMasterTableSources(List<String> tables) {
        	Map<IDataHandler, String[]> hm = new HashMap<IDataHandler, String[]>();
        	
        	List<String> theTables = new ArrayList<String>();
        	List<IDataHandler> masterDataHandlers = synchroHandler.getMasterDataHandlers();
        	for (IDataHandler dataHandler : masterDataHandlers) {
        		String[] monitoredTables = dataHandler.getMonitoredTables();
        		for (String monitoredTable : monitoredTables) {
        			for (String searchTable : tables) {
        				if (searchTable.equalsIgnoreCase(monitoredTable)) {
        					theTables.add(searchTable);
        				}
        			}
        		}
        		if (theTables.size() > 0) {
        			hm.put(dataHandler, theTables.toArray(new String[theTables.size()]));
        		}
        		theTables.clear();
        	}
        	
        	return hm;
        }
        
        private void syncDatabases(Map<IDataHandler, String[]> tablesToSync, Map<IDataHandler, Long> lastSyncCheckMap) throws RAFGException, SQLException, Exception {
//            if (LOCAL_DBG) {
//                AxisLogger.info("CacheController Syncing to cache database.");
//            }

        	List<Exception> exceptions = null;
        	
            if (tablesToSync != null) {
                for (Entry<IDataHandler, String[]> entry : tablesToSync.entrySet()) {
                    for (String table : entry.getValue()) {
                        try {
							synchroHandler.syncTable(entry.getKey().getConnection(), table);
						} catch (Exception e) {
							//do nothing now, keep the exception caught for processing after the loop.
							if (exceptions == null) {
								exceptions = new ArrayList<Exception>();
							}
							exceptions.add(e);
						}
                    }
                    lastSyncCheckMap.put(entry.getKey(), Calendar.getInstance().getTimeInMillis());
                }
            }
            
            if (exceptions != null && exceptions.size() > 0) {
            	for (Exception e : exceptions.subList(0, exceptions.size()-1)) {
            		//log all but last,  that is handled by whoever is calling this method...
    				if (! (e instanceof RAFGException || e instanceof SQLException)) {
    					RAFGLogger.error(e, (Object[]) null);
    				}
            	}
            	throw exceptions.get(exceptions.size()-1);
            }
        }


    }
}
